goog.provide('dkpoints.status.Status');

goog.require('dkpoints.status.Message');
goog.require('dkpoints.status.MessageGroup');
goog.require('dkpoints.status.templates');
goog.require('dkpoints.utils');

goog.require('goog.dom');
goog.require('goog.dom.ViewportSizeMonitor');
goog.require('goog.events');
goog.require('goog.math');
goog.require('goog.string');
goog.require('goog.style');
goog.require('goog.ui.Button');
goog.require('goog.ui.Component.EventType');
goog.require('goog.ui.CustomButton');
goog.require('goog.ui.decorate');



/**
 * The status panel, designed to be used as a singleton and passed to any module
 * that wants to use it.
 * @param {!Element} body The document body.
 * @param {!Element} bodyContent The #body_content element.
 * @constructor
 */
dkpoints.status.Status = function(body, bodyContent) {
  dkpoints.utils.installCSS('statuspanel');

  /**
   * This has to be a child of <body> instead of #body_content because
   * #body_content.innerHTML is frequently cleared and redrawn.
   * @type {Element}
   * @private
   */
  this.documentBody_ = body;

  /**
   * While the status panel is not a child of #body_content, its position is
   * relative to this element's top.
   * @type {Element}
   * @private
   */
  this.bodyContent_ = bodyContent;

  /**
   * The width for the status panel, in pixels
   * @type {number}
   * @const
   */
  this.WIDTH = 175;

  /**
   * A map of messages and message groups.
   * @type
   *    {Object.<string, dkpoints.status.Message|dkpoints.status.MessageGroup>}
   * @private
   */
  this.messageList_ = {};
  this.messageList_.count = 0;

  /**
   * Whether the status panel is currently on- or off-screen.  Not to be
   * confused with this.display_.
   * @type {boolean}
   * @private
   */
  this.shown_ = false;

  /**
   * Current value of this.statusPanel_.style.display.  Used as a cache to
   * prevent frequent DOM accesses during transitions.
   * @type {string}
   * @private
   */
  this.display_ = 'none';

  /**
   * @type {goog.dom.ViewportSizeMonitor}
   * @private
   */
  this.vsm_ = new goog.dom.ViewportSizeMonitor();
  goog.events.listen(this.vsm_, goog.events.EventType.RESIZE,
                     goog.bind(this.onVSMResize_, this), false, this);

  var statusEl = null;
  /**
   * The top-level div element for the status panel.
   * @type {Element}
   * @private
   */
  this.statusPanel_ = statusEl =
      goog.dom.createDom('div', {'id': 'statusPanel'});
  statusEl.style.display = this.display_ = 'none';
  statusEl.innerHTML = dkpoints.status.templates.loadBase({});
  body.appendChild(statusEl);

  /**
   * The content element for the status panel.  Groups and messages are added
   * here.
   * @type {Element}
   * @private
   */
  this.contentEl_ = goog.dom.getElement('status_content');

  // Local references for buttons to hide/clear/show the Status Panel.
  var hideBtn = null,
      clearBtn = null,
      showBtn = null;

  /**
   * @type {goog.ui.Component}
   * @private
   */
  this.hideStatusButton_ = hideBtn =
      goog.ui.decorate(goog.dom.getElement('statusCloseButton'));
  hideBtn.setCollapsed(goog.ui.ButtonSide.START);
  goog.events.listen(hideBtn, goog.ui.Component.EventType.ACTION,
                     goog.bind(this.animHide_, this), false, this);

  /**
   * @type {goog.ui.Component}
   * @private
   */
  this.clearStatusButton_ = clearBtn =
      goog.ui.decorate(goog.dom.getElement('statusClearButton'));
  clearBtn.setCollapsed(goog.ui.ButtonSide.END);
  goog.events.listen(clearBtn, goog.ui.Component.EventType.ACTION,
                     goog.bind(this.removeAll, this), false, this);

  /**
   * @type {Element}
   */
  var showStatusButton =
      soy.renderAsElement(dkpoints.status.templates.showStatusButton, {});
  body.insertBefore(showStatusButton, bodyContent);

  /**
   * @type {goog.ui.Component}
   * @private
   */
  this.showStatusButton_ = showBtn = goog.ui.decorate(showStatusButton);
  goog.events.listen(showBtn, goog.ui.Component.EventType.ACTION,
                     goog.bind(this.animShow_, this), false, this);

  /**
   * Temporary variable to signal to the resize method that
   * @type {boolean}
   * @private
   */
  this.initializing_ = true;
  this.onVSMResize_();
  delete this.initializing_;
};


/**
 * Calculates the status bar's size (fixed width x variable height, proporional
 * to viewport height) and position (25 pixels below the top of #body_content
 * and aligned to the far right screen edge).
 * @return {goog.math.Rect} The position and size of the status bar.
 * @private
 */
dkpoints.status.Status.prototype.calcSizeAndPos_ = function() {
  var viewportSize = this.vsm_.getSize(),
      width = this.WIDTH,
      top = goog.style.getPosition(this.bodyContent_).y + 25;

  return new goog.math.Rect((viewportSize.width - width - 5), top,
                            width, (viewportSize.height - top - 50));
};


/**
 * Callback for Viewport resizing.  Also called on initial loading of the
 * Status Panel to set up its size.
 * @param {Object=} event The resize event object passed for onResize events.
 * @private
 */
dkpoints.status.Status.prototype.onVSMResize_ = function(event) {
  // This method can be called when the VSM is initialized.  Only resize the
  // status panel if its being initialized or it is displayed.

  if (this.shown_ || this.initializing_) {
    var panel = this.statusPanel_,
        dims = this.calcSizeAndPos_();

    /**
     * @type {goog.math.Rect}
     * @private
     */
    this.dims_ = dims;

    goog.style.setSize(panel, new goog.math.Size(dims.width, dims.height));
    goog.style.setPosition(panel, dims.left, dims.top);
  }
};


/**
 * Animation for hiding the status window.
 * @private
 */
dkpoints.status.Status.prototype.animHide_ = function() {
  if (this.shown_) {
    var panel = this.statusPanel_,
        screenWidth = this.vsm_.getSize().width,
        panelLeft = parseInt(panel.style.left, 10),
        button = this.showStatusButton_;

    if (panelLeft < screenWidth) {
      panel.style.left = (panelLeft + 5) + 'px';
      setTimeout(goog.bind(this.animHide_, this), 1);
    }
    else {
      panel.style.display = this.display_ = 'none';
      this.shown_ = false;
      this.updateShowButton_();
    }
  }
};


/**
 * Animation for showing the status window.
 * @private
 */
dkpoints.status.Status.prototype.animShow_ = function() {
  var panel = this.statusPanel_,
      button = this.showStatusButton_,
      docBody = this.documentBody_;

  if (this.display_ !== 'block') {
    panel.style.display = this.display_ = 'block';
  }

  if (! this.shown_) {
    if (panel.parentNode !== this.documentBody_) {
      this.documentBody_.appendChild(panel);
    }

    var currLeft = parseInt(panel.style.left, 10),
        stopLeft = this.vsm_.getSize().width - this.WIDTH;

    if (currLeft > stopLeft) {
      var shift = Math.min(5, currLeft - stopLeft);
      panel.style.left = (currLeft - shift) + 'px';
      setTimeout(goog.bind(this.animShow_, this), 1);
    }
    else {
      this.shown_ = true;
      this.updateShowButton_();
    }
  }
};


/**
 * Updates the counter on the Show Status button.
 * @private
 */
dkpoints.status.Status.prototype.updateShowButton_ = function() {
  var count = this.messageList_.count,
      button = this.showStatusButton_;

  if (count === 0 || this.shown_) {
    button.addClassName('show-status-hidden');
  }
  else {
    var buttonEl = button.getElement(),
        suffix = count === 1 ? ' message)' : ' messages)';

    button.setCaption('<< Show Status (' + count + suffix);
    button.removeClassName('show-status-hidden');
    var dims = this.dims_,
        size = goog.style.getSize(buttonEl),
        width = size.width,
        height = size.height,
        left = dims.width + dims.left - width - 6,
        top = goog.style.getPosition(this.bodyContent_).y;

    goog.style.setPosition(buttonEl, left + 'px', top + 'px');
  }
};


/*
 * Public functions
 *
 */


/**
 * Add a message to the Status Panel.
 * @param {string} message The text to display.
 * @param {string} messageID The ID of the message.
 * @param {string=} groupName The name of the group to add the message to.
 * @param {number=} remDelay Optional interval after which the message will be
 *    removed.
 * @return {boolean} True if the message is added, false otherwise.
 */
dkpoints.status.Status.prototype.addMessage =
    function(message, messageID, groupName, remDelay) {

  var messageList = this.messageList_,
      success = true;

  // Add a prefix to avoid collisions between messages and groups.
  messageID = goog.string.startsWith(messageID, 'm_') ? messageID :
                                                        'm_' + messageID;

  if (goog.isDefAndNotNull(groupName)) {
    // Adding a message to a group.

    // Add a prefix to avoid collisions between messages and groups.
    var groupID = 'g_' + groupName,
        group = messageList[groupID];

    if (goog.isDef(group)) {
      // The group already exists.
      success = group.addMessage(message, messageID);
    }
    else {
      // Group doesn't exist yet.  Create one.
      group = messageList[groupID] =
          new dkpoints.status.MessageGroup(groupName, this.contentEl_);

      // Now add the message to the group.
      success = group.addMessage(message, messageID);
    }

    if (goog.isDefAndNotNull(remDelay)) {
      group.remTimeouts = group.remTimeouts || {};
      group.remTimeouts[messageID] =
          setTimeout(goog.bind(this.removeMessage, this,
                               messageID, groupName, undefined),
                     remDelay);
    }
  }
  else { // No group name.  This is a solo message.
    var messageObj = messageList[messageID] =
        new dkpoints.status.Message(message, messageID, this.contentEl_);

    if (goog.isDefAndNotNull(remDelay)) {
      messageObj.remTimeout =
          setTimeout(goog.bind(this.removeMessage, this, messageID), remDelay);
    }
  }

  if (success) {
    messageList.count++;
    this.animShow_();
  }

  return success;
};


/**
 * Update an existing message with new text.
 * @param {string} message The new message text.
 * @param {string} messageID The ID of the message to be udpated.
 * @param {string=} groupName The group this message is in.
 * @param {number=} remDelay Optional interval after which to remove the
 *    message.
 * @return {boolean} True if a message was updated; false if not.
 */
dkpoints.status.Status.prototype.updateMessage =
    function(message, messageID, groupName, remDelay) {

  messageID = goog.string.startsWith(messageID, 'm_') ? messageID :
                                                        'm_' + messageID;
  var messageList = this.messageList_,
      success = true;

  if (goog.isDefAndNotNull(groupName)) {
    var groupID = 'g_' + groupName,
        group = messageList[groupID];

    if (goog.isDef(group)) {
      success = group.updateMessage(message, messageID);

      if (goog.isDefAndNotNull(remDelay)) {
        group.remTimeouts = group.remTimeouts || {};

        var timeout = group.remTimeouts[messageID];
        if (goog.isDef(timeout)) {
          // A remove timeout was already set for this message.  Clear it first.
          clearTimeout(timeout);
        }

        group.remTimeouts[messageID] =
            setTimeout(goog.bind(this.removeMessage, this,
                                 messageID, groupName),
                       remDelay);
      }
    }
    else {
      // Group doesn't exist, so the message cannot be updated.
      return false;
    }
  }
  else {
    // No group.  This is a solo message.
    var messageObj = messageList[messageID];
    if (goog.isDef(messageObj)) {
      success = messageObj.updateMessage(message);

      if (goog.isDefAndNotNull(remDelay)) {
        var timeout = messageObj.remTimeout;
        if (goog.isDef(timeout)) {
          clearTimeout(timeout);
        }

        messageObj.remTimeout =
            setTimeout(goog.bind(this.removeMessage, this, messageID),
                       remDelay);
      }
    }
    else {
      // Message doesn't exist.
      return false;
    }
  }

  if (success) {
    this.animShow_();
  }

  return success;
};


/**
 * Remove a message from the Status Panel.
 * @param {string} messageID The ID of the message to be removed.
 * @param {string=} groupName The group the message belongs to.
 * @param {number=} remDelay Optional delay after which the message will be
 *    removed.
 * @return {boolean} True if the message was successfully removed, or a timeout
 *    successfully set; false otherwise.
 */
dkpoints.status.Status.prototype.removeMessage =
    function(messageID, groupName, remDelay) {

  messageID = goog.string.startsWith(messageID, 'm_') ? messageID :
                                                        'm_' + messageID;
  var messageList = this.messageList_,
      success = true;

  if (goog.isDefAndNotNull(groupName)) {
    var groupID = 'g_' + groupName,
        group = messageList[groupID];

    if (goog.isDef(group)) {
      // The group exists.

      if (goog.isDefAndNotNull(remDelay)) {
        // Don't remove it yet; set up a timeout.
        group.remTimeouts = group.remTimeouts || {};
        var timeout = group.remTimeouts[messageID];
        if (goog.isDef(timeout)) {
          clearTimeout(timeout);
        }

        group.remTimeouts[messageID] =
            setTimeout(goog.bind(this.removeMessage, this, messageID,
                                 groupName),
                       remDelay);
        return true;
      }
      else {
        if (group.remTimeouts && group.remTimeouts[messageID]) {
          clearTimeout(group.remTimeouts[messageID]);
        }

        success = group.removeMessage(messageID);
        if (group.getCount() === 0) {
          group.dispose();
          delete messageList[groupID];
        }
      }
    }
    else {
      // No group by that name.
      return false;
    }
  }
  else {
    // No groupName given.
    var messageObj =
        /** @type {dkpoints.status.Message} */ (messageList[messageID]);

    if (goog.isDef(messageObj)) {
      if (messageObj.remTimeout) {
        clearTimeout(messageObj.remTimeout);
      }

      if (goog.isDefAndNotNull(remDelay)) {
        messageObj.remDelay =
            setTimeout(goog.bind(this.removeMessage, this, messageID),
                       remDelay);
        return true;
      }
      else {
        success = messageObj.dispose();
        delete messageList[messageID];
      }
    }
    else {
      // Message didn't exist.
      return false;
    }
  }

  if (success) {
    messageList.count--;
    this.updateShowButton_();
    if (messageList.count === 0) {
      this.animHide_();
    }
  }
  return success;
};


/**
 * Remove all messages.
 * @return {number} The number of messages removed.
 */
dkpoints.status.Status.prototype.removeAll = function() {
  var removed = this.messageList_.count;

  this.contentEl_.innerHTML = '';
  this.messageList_ = {};
  this.messageList_.count = 0;

  this.updateShowButton_();
  this.animHide_();

  return removed;
};
